/*
 * PacketManager.java
 *
 * Created on May 10, 2007, 1:04 PM
 *
 *
 * Copyright (c) 2008 Golden T Studios.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.golden.gamedev.engine.network;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import com.golden.gamedev.engine.network.manipulator.DataEncryption;
import com.golden.gamedev.engine.network.manipulator.DataObfuscation;
import com.golden.gamedev.engine.network.packet.NetworkMessage;
import com.golden.gamedev.engine.network.packet.NetworkPing;

/**
 *
 * @author Paulus Tuerah
 */
public class PacketManager {

	public int				BUFFER_SIZE	= 2048;		// bigger than any packable

	private short[]			registeredID		= new short[0];		// registered packet id
	private Object[]		registeredObject	= new Object[0];	// registered packet object (NetworkPacket class or object)


	// for security, we encrypt and obfuscate the packet
	private DataManipulator[]	manipulator = new DataManipulator[] {
		new DataEncryption(), new DataObfuscation() };


 /****************************************************************************/
 /******************************* CONSTRUCTOR ********************************/
 /****************************************************************************/

	/**
	 * Creates a new instance of PacketManager
	 */
	public PacketManager() {
	}


	public void registerPacket(short id, NetworkPacket packet) {
//		if (NetworkConfig.DEBUG) {
//			System.out.println("Register ID " + id + " for packet " + packet);
//		}

		if (packet.getID() == NetworkPacket.NULL_ID) {
			throw new RuntimeException("Packet ID " + packet + " must be set " +
				"either by passing NetworkPacket.setID(short) or " +
				"by overriding NetworkPacket.getID() method");
		}

		for (int i=0;i < registeredID.length;i++) {
			if (registeredID[i] == id) {
				throw new RuntimeException("ID " + id + " is already registered with " + registeredObject[i]);
			}
		}

		registeredID			= (short[]) NetworkUtil.expand(registeredID, 1);
		registeredObject		= (Object[]) NetworkUtil.expand(registeredObject, 1);

		int len = registeredID.length-1;
		registeredID[len]		= id;
		registeredObject[len]	= packet;
	}

	public void registerPacket(short id, Class packetClass) {
//		if (NetworkConfig.DEBUG) {
//			String className = packetClass.getName();
//			int index = className.lastIndexOf(".");
//			if (index != -1) {
//				className = className.substring(index + 1);
//			}
//
//			System.out.println("Register ID " + id + " for packet class " + className);
//		}

		if (!NetworkPacket.class.isAssignableFrom(packetClass)) {
			throw new RuntimeException("Unable to register packet class " + packetClass + ", not subclass of NetworkPacket class.");
		}

		for (int i=0;i < registeredID.length;i++) {
			if (registeredID[i] == id) {
				throw new RuntimeException("ID " + id + " is already registered with " + registeredObject[i]);
			}
		}

		registeredID			= (short[]) NetworkUtil.expand(registeredID, 1);
		registeredObject		= (Object[]) NetworkUtil.expand(registeredObject, 1);

		int len = registeredID.length-1;
		registeredID[len]		= id;
		registeredObject[len]	= packetClass;
	}


	public synchronized byte[] pack(NetworkPacket packet) throws IOException {
		ByteArrayOutputStream baout	= new ByteArrayOutputStream(); // the byte array stream
		DataOutputStream output		= new DataOutputStream(baout);


		// write the packet id
		output.writeShort(getPacketID(packet));

		// compress the packet
		if (packet.isCompressed()) {
			output = compress(output);
		}

		// write the packet data
		packet.writePacket(output);


		output.close();

		byte[] data = baout.toByteArray();

		// manipulate packet for security
		if (manipulator != null) {
			try {
				for (int i=0;i < manipulator.length;i++) {
					data = manipulator[i].manipulate(data);
				}

			} catch (Exception ex) {
				ex.printStackTrace();
				throw new IOException(ex.getMessage());
			}
		}

		return data;
	}

	public synchronized NetworkPacket unpack(byte[] data) throws IOException {
		// demanipulate packet data first
		if (manipulator != null) {
			try {
				for (int i=manipulator.length-1;i >= 0;i--) {
					data = manipulator[i].demanipulate(data);
				}

			} catch (Exception ex) {
				ex.printStackTrace();
				throw new IOException(ex.getMessage());
			}
		}


		ByteArrayInputStream bain	= new ByteArrayInputStream(data);
		DataInputStream input		= new DataInputStream(bain);


		// read the packet id
		short packetID = input.readShort();

		// create the packet
		NetworkPacket packet = null;

		// read the packet data
		if (packetID == NetworkPing.ID) {
			// ping packet
			packet = NetworkPing.getInstance();

		} else {
			// network packet
			packet = (packetID == NetworkMessage.ID) ?
				new NetworkMessage() : createPacket(packetID);

			// decompress
			if (packet.isCompressed()) {
				input = decompress(input);
			}

			// construct the packet
			packet.readPacket(input);
		}


		input.close();

		return packet;
	}


	protected short getPacketID(NetworkPacket packet) {
		if (packet.getID() != NetworkPacket.NULL_ID) {
			// packet id is already set
			// for example on new NetworkObject(UNIQUE_ID);
			return packet.getID();
		}


		// packet id is not set (null id)
		// we need to get the real packet id
		// from the type of the packet class

		Class packetClass = packet.getClass();

		for (int i=0;i < registeredID.length;i++) {
			if (packetClass.equals(registeredObject[i])) {
				return registeredID[i];
			}
		}

		throw new RuntimeException(
			packetClass + " class has not been registered.\n" +
			"Use NetworkConfig.registerPacket(short id, Class packetClass) to register the packet id.");
	}

	protected NetworkPacket createPacket(short id) {
		NetworkPacket packet = null;

		// construct the packet based on its id
		// if it's a class, create new instance
		// if it's an object, we clone it
		for (int i=0;i < registeredID.length;i++) {
			if (registeredID[i] == id) {
				Object type = registeredObject[i];

				if (type instanceof Class) {
					// class type
					// create new instance
					try {
						packet = (NetworkPacket) ((Class) type).newInstance();

					} catch (Exception ex) {
						throw new RuntimeException(ex);
					}

				} else {
					// NetworkPacket object, clone from the object
					try {
						packet = (NetworkPacket) ((NetworkPacket) type).clone();

					} catch (Exception ex) {
						throw new RuntimeException(ex);
					}
				}

				break;
			}
		}

		if (packet == null) {
			throw new RuntimeException(
				"ID " + id + " has not been registered.\n" +
				"Use NetworkConfig.registerPacket(short id, Class packetClass) to register the packet id.");
		}

		packet.setID(id);

		return packet;
	}


	protected DataOutputStream compress(DataOutputStream output) throws IOException {
		return new DataOutputStream(new GZIPOutputStream(output, BUFFER_SIZE));
	}

	protected DataInputStream decompress(DataInputStream input) throws IOException {
		return new DataInputStream(new GZIPInputStream(input, BUFFER_SIZE));
	}


	public short[] getRegisteredID() {
		return registeredID;
	}

	public Object[] getRegisteredObject() {
		return registeredObject;
	}


	public DataManipulator[] getDataManipulator() {
		return manipulator;
	}

	public void setDataManipulator(DataManipulator[] dataManipulator) {
		this.manipulator = dataManipulator;
	}

}